/*
 * Copyright (C) 2009, 2010, B3log Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.b3log.nide.util.parser.java.source;

import java.util.logging.Level;
import org.b3log.nide.util.parser.marker.IdentifierMarker;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.LineMap;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.util.Trees;
import com.sun.tools.javac.api.JavacTaskImpl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import javax.lang.model.util.Elements;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.ToolProvider;
import org.b3log.nide.model.source.java.JavaSourceObject;
import org.b3log.nide.util.parser.marker.HighlightingMarker;

/**
 * Java abstract syntax tree.
 *
 * @author <a href="mailto:DL88250@gmail.com">Liang Ding</a>
 * @version 1.0.0.5, Nov 29, 2010
 */
public final class JavaAST {

    /**
     * Logger.
     */
    private static final Logger LOGGER =
            Logger.getLogger(JavaAST.class.getName());
    /**
     * Java compiler.
     */
    private JavaCompiler javaCompiler;
    /**
     * Class visitor.
     */
    private ClassVisitor classVisitor;
    /**
     * Java source code.
     */
    private String javaSourceCode;
    /**
     * Line map of source code.
     */
    private LineMap lineMap;
    /**
     * Comment visitor.
     */
    private CommentVisitor commentVisitor;
    /**
     * String visitor.
     */
    private StringVisitor stringVisitor;
    /**
     * Character visitor.
     */
    private CharVisitor charVisitor;

    /**
     * Constructs a Java abstract syntax tree for the specified Java source
     * code.
     *
     * @param javaSourceCode the specified Java source code
     */
    public JavaAST(final String javaSourceCode) {
        this.javaSourceCode = javaSourceCode;

        javaCompiler = ToolProvider.getSystemJavaCompiler();

        if (null == javaCompiler) {
            throw new RuntimeException("No Java compiler is provided!");
        }

        commentVisitor = new CommentVisitor(javaSourceCode);
        stringVisitor = new StringVisitor(javaSourceCode);
        charVisitor = new CharVisitor(javaSourceCode);

        visit();
    }

    /**
     * Gets the highlighting source marker at the specified line number and
     * column number.
     *
     * @param lineNum the specified line number(beginning at 1)
     * @param columnNum the specified column number(beginning at 1)
     * @return highlighting source marker if found, {@code null} otherwise
     */
    public HighlightingMarker getHighlightingMarker(final int lineNum,
                                                    final int columnNum) {
        final long position = lineMap.getPosition(lineNum, columnNum);

        Set<HighlightingMarker> highlightingMarkers =
                classVisitor.getHighlightingMarkers();

        for (final HighlightingMarker highlightingMarker : highlightingMarkers) {
            if (highlightingMarker.getPosition() == position) {
//                LOGGER.trace("Found a highlighting source marker[color="
//                             + highlightingMarker.getColor()
//                             + ", position="
//                             + highlightingMarker.getPosition() + "]");

                return highlightingMarker;
            }
        }

        highlightingMarkers = stringVisitor.getHighlightingMarkers();
        for (final HighlightingMarker highlightingMarker : highlightingMarkers) {
            if (highlightingMarker.getPosition() == position) {

                return highlightingMarker;
            }
        }

        highlightingMarkers = charVisitor.getHighlightingMarkers();
        for (final HighlightingMarker highlightingMarker : highlightingMarkers) {
            if (highlightingMarker.getPosition() == position) {

                return highlightingMarker;
            }
        }

        highlightingMarkers = commentVisitor.getHighlightingMarkers();
        for (final HighlightingMarker highlightingMarker : highlightingMarkers) {
            if (highlightingMarker.getPosition() == position) {

                return highlightingMarker;
            }
        }

        return null;
    }

    /**
     * Gets the identifier source marker at the specified line number and
     * end column number.
     *
     * @param lineNum the specified line number(beginning at 1)
     * @param endColumnNum the specified end column number(beginning at 1)
     * @return identifier source marker if found, {@code null} otherwise
     */
    public IdentifierMarker getIdentifierMarker(final int lineNum,
                                                final int endColumnNum) {
        final long position = lineMap.getPosition(lineNum, endColumnNum);

        final Set<IdentifierMarker> identifierMarkers =
                classVisitor.getIdentifierMarkers();

        for (final IdentifierMarker identifierMarker : identifierMarkers) {
            if (identifierMarker.getPosition() == position) {
                LOGGER.log(Level.FINER,
                           "Found an identifier source marker[identifier={0}, position={1}]",
                           new Object[]{identifierMarker.getIdentifierName(),
                                        identifierMarker.getPosition()});

                return identifierMarker;
            }
        }

        return null;
    }

    /**
     * Visits this tree by {@linkplain #classVisitor}.
     */
    private void visit() {
        final String fileName = "App.java"; // TODO: file name????
        final JavaFileObject file = new JavaSourceObject(fileName,
                                                         javaSourceCode);
        final Iterable<? extends JavaFileObject> compilationUnits =
                Arrays.asList(file);

        final CompilationTask task = javaCompiler.getTask(
                null, null, null, null, null, compilationUnits);
        final JavacTaskImpl javacTask = (JavacTaskImpl) task;
        final Elements elements = javacTask.getElements();

        Iterable<? extends CompilationUnitTree> compilationUnitTrees = null;
        try {
            compilationUnitTrees = javacTask.parse();
            javacTask.analyze();
        } catch (final IOException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        }

        classVisitor = new ClassVisitor(elements, Trees.instance(task));

        final CompilationUnitTree tree = compilationUnitTrees.iterator().next();
        lineMap = tree.getLineMap();

        classVisitor.scan(tree, null);
    }

    /**
     * Gets imports.
     *
     * @return imports
     */
    public List<String> getImports() {
        final List<String> ret = new ArrayList<String>();
        final List<MemberSelectTree> imports = classVisitor.getImports();

        for (final MemberSelectTree memberSelectTree : imports) {
            ret.add(memberSelectTree.getExpression() + "."
                    + memberSelectTree.getIdentifier());
        }

        return ret;
    }
}
